﻿#region License
/*  YanYeovil
    Copyright (c) 2014, Alexey A. Popov
    All rights reserved.

    Redistribution and use in source and binary forms, with or without modification, are
    permitted provided that the following conditions are met:

    - Redistributions of source code must retain the above copyright notice, this list
      of conditions and the following disclaimer.

    - Redistributions in binary form must reproduce the above copyright notice, this list
      of conditions and the following disclaimer in the documentation and/or other materials
      provided with the distribution.

    - Neither the propertyName of the Alexey A. Popov nor the names of its contributors may be used to
      endorse or promote products derived from this software without specific prior written
      permission.

    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS *AS IS* AND ANY EXPRESS
    OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
    AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
    CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
    DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
    DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
    IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
    OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#endregion

using System;
using System.Collections.Generic;
using System.Data;

namespace Pvax.Data
{
    /// <summary>
    /// Extends the <see cref="IDbConnection"/> interface with methods that help to store and retrieve
    /// .Net objects in an ADO.Net database.
    /// </summary>
    /// <remarks>
    /// The extension methods work only with objects' state exposed by public properties. Moreover, they
    /// do not change read only properties or properties of explicitly implemented interfaces.
    /// </remarks>
    public static class DbConnectionExtender
    {
        static Dictionary<Type, TypeMapping> mappings = new Dictionary<Type, TypeMapping>();

        static TypeMapping GetMappingForType(Type type)
        {
            TypeMapping result;
            if(!mappings.TryGetValue(type, out result))
            {
                result = new TypeMapping(type);
                mappings.Add(type, result);
            }
            return result;
        }

        static void PullFromDb(object instance, PropertyMapping field, IDataReader reader)
        {
            try
            {
                field.Pull(instance, reader);
            }
            catch(Exception ex)
            {
                throw new ObjectMappingException(Properties.Resources.ErrUnhandledEx, ex);
            }
        }

        static void PushToDb(object instance, PropertyMapping field, IDbCommand command, int index)
        {
            try
            {
                IDataParameter parameter = (IDataParameter)command.Parameters[index];
                field.Push(instance, parameter);
            }
            catch(Exception ex)
            {
                throw new ObjectMappingException(Properties.Resources.ErrUnhandledEx, ex);
            }
        }

        // Mark the loaded record as clean
        static void Clean(object instance)
        {
            if(instance is IDbStoreable)
                ((IDbStoreable)instance).Clear();
        }

        static bool IsDirty(object instance)
        {
            if(instance is IDbStoreable)
                return ((IDbStoreable)instance).IsDirty;
            return true;
        }

        // Mark the loaded record as present in the DB
        static void Touch(object instance)
        {
            if(instance is IDbStoreable)
                ((IDbStoreable)instance).Touch();
        }

        // Check if the record is not in the DB
        static bool IsNew(object instance)
        {
            if(instance is IDbStoreable)
                return ((IDbStoreable)instance).IsNew;
            return true;
        }

        static string DefaultTableNameMapper(string name)
        {
            return name + "s";
        }

        static T DefaultBuilder<T>()
            where T: class, new()
        {
            return new T();
        }

        /// <summary>
        /// Selects all objects from a mapped table in the DB.
        /// </summary>
        /// <typeparam name="T">The type of the objects.</typeparam>
        /// <param name="connection">The database connection.</param>
        /// <returns>
        /// An an object that implements the <see cref="IEnumerable{T}"/> interface and lazily
        /// fetches all the objects form the database table.
        /// </returns>
        public static IEnumerable<T> GetAll<T>(this IDbConnection connection)
            where T: class, new()
        {
            return GetAll<T>(connection, DefaultBuilder<T>, DefaultTableNameMapper);
        }

        /// <summary>
        /// Selects all objects from a mapped table in the DB.
        /// </summary>
        /// <typeparam name="T">The type of the objects.</typeparam>
        /// <param name="connection">The database connection.</param>
        /// <param name="builder">The function delegate used to create objects of type <typeparamref name="T"/>.</param>
        /// <returns>
        /// An an object that implements the <see cref="IEnumerable{T}"/> interface and lazily
        /// fetches all the objects form the database table.
        /// </returns>
        public static IEnumerable<T> GetAll<T>(this IDbConnection connection, Func<T> builder)
            where T: class
        {
            return GetAll<T>(connection, builder, DefaultTableNameMapper);
        }

        /// <summary>
        /// Selects all objects from a mapped table in the DB.
        /// </summary>
        /// <typeparam name="T">The type of the objects.</typeparam>
        /// <param name="connection">The database connection.</param>
        /// <param name="tableNameMapper">
        /// The function that maps a CLR class name to a database table name.
        /// </param>
        /// <returns>
        /// An an object that implements the <see cref="IEnumerable{T}"/> interface and lazily
        /// fetches all the objects form the database table.
        /// </returns>
        public static IEnumerable<T> GetAll<T>(this IDbConnection connection, Func<string, string> tableNameMapper)
            where T: class, new()
        {
            return GetAll<T>(connection, DefaultBuilder<T>, tableNameMapper);
        }

        /// <summary>
        /// Selects all objects from a mapped table in the DB.
        /// </summary>
        /// <typeparam name="T">The type of the objects.</typeparam>
        /// <param name="connection">The database connection.</param>
        /// <param name="builder">The function delegate used to create objects of type <typeparamref name="T"/>.</param>
        /// <param name="tableNameMapper">
        /// The function that maps a CLR class name to a database table name.
        /// </param>
        /// <returns>
        /// An an object that implements the <see cref="IEnumerable{T}"/> interface and lazily
        /// fetches all the objects form the database table.
        /// </returns>
        public static IEnumerable<T> GetAll<T>(this IDbConnection connection, Func<T> builder, Func<string, string> tableNameMapper)
            where T: class
        {
            if(null == connection)
                throw new ArgumentNullException("connection", Properties.Resources.ErrNullParam);
            if(null == builder)
                throw new ArgumentNullException("builder", Properties.Resources.ErrNullParam);
            if(null == tableNameMapper)
                throw new ArgumentNullException("tableNameMapper", Properties.Resources.ErrNullParam);

            TypeMapping tm = GetMappingForType(typeof(T));
            using(IDbCommand command = tm.CreateSelectAllCommand(connection, tableNameMapper))
            using(IDataReader reader = command.ExecuteReader())
            {
                while(reader.Read())
                {
                    T result = builder();
                    foreach(var kv in tm.Properties)
                        PullFromDb(result, kv.Value, reader);
                    Clean(result);
                    Touch(result);
                    yield return result;
                }
            }
        }

        static T Pull<T>(Func<T> builder, TypeMapping tm, IDataReader reader) where T: class
        {
            T result = builder();
            foreach(var kv in tm.Properties)
                PullFromDb(result, kv.Value, reader);
            Clean(result);
            Touch(result);
            return result;
        }

        /// <summary>
        /// Selects an object form the database by it's primary key.
        /// </summary>
        /// <typeparam name="T">The type of the objects.</typeparam>
        /// <typeparam name="TId">The type of the primary key property.</typeparam>
        /// <param name="connection">The database connection.</param>
        /// <param name="primaryKeyValue">The value of the primary key.</param>
        /// <returns>
        /// The reference to the object ftched form the database; <c>null</c> othewise.
        /// </returns>
        public static T Get<T, TId>(this IDbConnection connection, TId primaryKeyValue)
            where T: class, new()
        {
            return Get<T, TId>(connection, DefaultBuilder<T>, primaryKeyValue, DefaultTableNameMapper);
        }

        /// <summary>
        /// Selects an object form the database by it's primary key.
        /// </summary>
        /// <typeparam name="T">The type of the objects.</typeparam>
        /// <typeparam name="TId">The type of the primary key property.</typeparam>
        /// <param name="connection">The database connection.</param>
        /// <param name="builder">The function delegate used to create objects of type <typeparamref name="T"/>.</param>
        /// <param name="primaryKeyValue">The value of the primary key.</param>
        /// <returns>
        /// The reference to the object ftched form the database; <c>null</c> othewise.
        /// </returns>
        public static T Get<T, TId>(this IDbConnection connection, Func<T> builder, TId primaryKeyValue)
            where T: class
        {
            return Get<T, TId>(connection, builder, primaryKeyValue, DefaultTableNameMapper);
        }

        /// <summary>
        /// Selects an object form the database by it's primary key.
        /// </summary>
        /// <typeparam name="T">The type of the objects.</typeparam>
        /// <typeparam name="TId">The type of the primary key property.</typeparam>
        /// <param name="connection">The database connection.</param>
        /// <param name="primaryKeyValue">The value of the primary key.</param>
        /// <param name="tableNameMapper">
        /// The function that maps a CLR class name to a database table name.
        /// </param>
        /// <returns>
        /// The reference to the object ftched form the database; <c>null</c> othewise.
        /// </returns>
        public static T Get<T, TId>(this IDbConnection connection, TId primaryKeyValue, Func<string, string> tableNameMapper)
            where T: class, new()
        {
            return Get<T, TId>(connection, DefaultBuilder<T>, primaryKeyValue, tableNameMapper);
        }

        /// <summary>
        /// Selects an object form the database by it's primary key.
        /// </summary>
        /// <typeparam name="T">The type of the objects.</typeparam>
        /// <typeparam name="TId">The type of the primary key property.</typeparam>
        /// <param name="connection">The database connection.</param>
        /// <param name="builder">The function delegate used to create objects of type <typeparamref name="T"/>.</param>
        /// <param name="primaryKeyValue">The value of the primary key.</param>
        /// <param name="tableNameMapper">
        /// The function that maps a CLR class name to a database table name.
        /// </param>
        /// <returns>
        /// The reference to the object ftched form the database; <c>null</c> othewise.
        /// </returns>
        public static T Get<T, TId>(this IDbConnection connection, Func<T> builder, TId primaryKeyValue, Func<string, string> tableNameMapper)
            where T: class
        {
            if(null == connection)
                throw new ArgumentNullException("connection", Properties.Resources.ErrNullParam);
            if(null == builder)
                throw new ArgumentNullException("builder", Properties.Resources.ErrNullParam);
            if(null == tableNameMapper)
                throw new ArgumentNullException("tableNameMapper", Properties.Resources.ErrNullParam);

            TypeMapping tm = GetMappingForType(typeof(T));
            PropertyMapping keyMapping = tm.PrimaryKey;
            using(IDbCommand command = tm.CreateSelectCommand(connection, tableNameMapper))
            {
                ((IDataParameter)command.Parameters[command.Parameters.Count - 1]).Value = primaryKeyValue;
                using(IDataReader reader = command.ExecuteReader())
                {
                    while(reader.Read())
                    {
                        T result = Pull<T>(builder, tm, reader);
                        return result;
                    }
                }
            }
            return default(T); // Actually, a typed null
        }

        /// <summary>
        /// Selects objects form the database based on field equality criterion.
        /// </summary>
        /// <typeparam name="T">The type of the data object</typeparam>
        /// <param name="connection">The database connection.</param>
        /// <param name="columnName">The name of the column/property.</param>
        /// <param name="value">The value to compare the column/property.</param>
        /// <returns>
        /// An an object that implements the <see cref="IEnumerable{T}"/> interface and lazily
        /// fetches selected objects form the database table.
        /// </returns>
        public static IEnumerable<T> GetBy<T>(this IDbConnection connection, string columnName, object value)
            where T: class, new()
        {
            return GetBy<T>(connection, DefaultBuilder<T>, columnName, value, DefaultTableNameMapper);
        }

        /// <summary>
        /// Selects objects form the database based on field equality criterion.
        /// </summary>
        /// <typeparam name="T">The type of the data object</typeparam>
        /// <param name="connection">The database connection.</param>
        /// <param name="builder">The function delegate used to create objects of type <typeparamref name="T"/>.</param>
        /// <param name="columnName">The name of the column/property.</param>
        /// <param name="value">The value to compare the column/property.</param>
        /// <returns>
        /// An an object that implements the <see cref="IEnumerable{T}"/> interface and lazily
        /// fetches selected objects form the database table.
        /// </returns>
        public static IEnumerable<T> GetBy<T>(this IDbConnection connection, Func<T> builder, string columnName, object value)
            where T: class
        {
            return GetBy<T>(connection, builder, columnName, value, DefaultTableNameMapper);
        }

        /// <summary>
        /// Selects objects form the database based on field equality criterion.
        /// </summary>
        /// <typeparam name="T">The type of the data object</typeparam>
        /// <param name="connection">The database connection.</param>
        /// <param name="builder">The function delegate used to create objects of type <typeparamref name="T"/>.</param>
        /// <param name="columnName">The name of the column/property.</param>
        /// <param name="value">The value to compare the column/property.</param>
        /// <param name="tableNameMapper">
        /// The function that maps a CLR class name to a database table name.
        /// </param>
        /// <returns>
        /// An an object that implements the <see cref="IEnumerable{T}"/> interface and lazily
        /// fetches selected objects form the database table.
        /// </returns>
        public static IEnumerable<T> GetBy<T>(this IDbConnection connection, Func<T> builder, string columnName, object value, Func<string, string> tableNameMapper)
            where T: class
        {
            if(null == connection)
                throw new ArgumentNullException("connection", Properties.Resources.ErrNullParam);
            if(null == builder)
                throw new ArgumentNullException("builder", Properties.Resources.ErrNullParam);
            if(String.IsNullOrEmpty(columnName))
                throw new ArgumentNullException("columnName", Properties.Resources.ErrNullParam);
            if(null == tableNameMapper)
                throw new ArgumentNullException("tableNameMapper", Properties.Resources.ErrNullParam);

            TypeMapping tm = GetMappingForType(typeof(T));
            PropertyMapping field = tm.Properties[columnName];
            if(null == field)
                throw new ObjectMappingException(String.Format(Properties.Resources.ErrNoMapping, columnName, tm.Type.FullName));
            using(IDbCommand command = tm.CreateSelectByCommand(connection, field, tableNameMapper))
            {
                ((IDataParameter)command.Parameters[0]).Value = value;
                using(IDataReader reader = command.ExecuteReader())
                {
                    while(reader.Read())
                    {
                        T result = Pull<T>(builder, tm, reader);
                        yield return result;
                    }
                }
            }
        }

        //// Implementation, doesn't check parameters
        static bool AddImpl<T>(IDbConnection connection, T instance, Func<string, string> tableNameMapper) where T: class
        {
            //// If the record is already in the DB, do nothing
            if(!IsNew(instance))
                return false;
            TypeMapping tm = GetMappingForType(typeof(T));
            using(IDbCommand command = tm.CreateInsertCommand(connection, tableNameMapper))
            {
                int index = 0;
                foreach(var kv in tm.Properties)
                    PushToDb(instance, kv.Value, command, index++);
                int result = command.ExecuteNonQuery();
                if(0 != result)
                {
                    Clean(instance);
                    Touch(instance);
                    return true;
                }
                else
                    return false;
            }
        }

        /// <summary>
        /// Inserts an object into the database.
        /// </summary>
        /// <typeparam name="T">The type of the objects.</typeparam>
        /// <param name="connection">The database connection.</param>
        /// <param name="instance">The object to insert into the database.</param>
        /// <returns><c>true</c> if object was successfully insterted; <c>false</c> otherwise.</returns>
        /// <remarks>
        /// Note that the return value <c>false</c> does not mean an error. If in the database there
        /// alreay is an object with the same primary key, it won't be inserted.
        /// </remarks>
        public static bool Add<T>(this IDbConnection connection, T instance)
            where T: class
        {
            return Add<T>(connection, instance, DefaultTableNameMapper);
        }

        /// <summary>
        /// Inserts an object into the database.
        /// </summary>
        /// <typeparam name="T">The type of the objects.</typeparam>
        /// <param name="connection">The database connection.</param>
        /// <param name="instance">The object to insert into the database.</param>
        /// <param name="tableNameMapper">
        /// The function that maps a CLR class name to a database table name.
        /// </param>
        /// <returns><c>true</c> if object was successfully insterted; <c>false</c> otherwise.</returns>
        /// <remarks>
        /// Note that the return value <c>false</c> does not mean an error. If the database
        /// already has an object with the same primary key, it won't be inserted.
        /// </remarks>
        public static bool Add<T>(this IDbConnection connection, T instance, Func<string, string> tableNameMapper)
            where T: class
        {
            if(null == connection)
                throw new ArgumentNullException("connection", Properties.Resources.ErrNullParam);
            if(null == instance)
                throw new ArgumentNullException("instance", Properties.Resources.ErrNullParam);
            if(null == tableNameMapper)
                throw new ArgumentNullException("tableNameMapper", Properties.Resources.ErrNullParam);

            return AddImpl<T>(connection, instance, tableNameMapper);
        }

        /// <summary>
        /// Adds a batch of objects to the database.
        /// </summary>
        /// <typeparam name="T">The type of the objects.</typeparam>
        /// <param name="connection">The database connection.</param>
        /// <param name="collection">A collection of the <typeparamref name="T"/> collection.</param>
        public static void AddRange<T>(this IDbConnection connection, IEnumerable<T> collection)
            where T: class
        {
            AddRange<T>(connection, collection, DefaultTableNameMapper);
        }

        /// <summary>
        /// Adds a batch of objects to the database.
        /// </summary>
        /// <typeparam name="T">The type of the objects.</typeparam>
        /// <param name="connection">The database connection.</param>
        /// <param name="collection">A collection of the <typeparamref name="T"/> data objects.</param>
        /// <param name="tableNameMapper">
        /// The function that maps a CLR class name to a database table name.
        /// </param>
        public static void AddRange<T>(this IDbConnection connection, IEnumerable<T> collection, Func<string, string> tableNameMapper)
            where T: class
        {
            if(null == connection)
                throw new ArgumentNullException("connection", Properties.Resources.ErrNullParam);
            if(null == collection)
                throw new ArgumentNullException("collection", Properties.Resources.ErrNullParam);
            if(null == tableNameMapper)
                throw new ArgumentNullException("tableNameMapper", Properties.Resources.ErrNullParam);

            using(IDbTransaction transaction = connection.BeginTransaction())
            {
                try
                {
                    foreach(var instance in collection)
                        AddImpl<T>(connection, instance, tableNameMapper);
                }
                catch
                {
                    transaction.Rollback();
                    throw;
                }
                transaction.Commit();
            }
        }

        /// <summary>
        /// Deletes an object form the database by it's primary key.
        /// </summary>
        /// <typeparam name="T">The type of the objects.</typeparam>
        /// <param name="connection">The database connection.</param>
        /// <param name="instance">The object to delete</param>
        /// <returns><c>true</c> if the object has been successfuly deleted; <c>false</c> otherwise.</returns>
        public static bool Remove<T>(this IDbConnection connection, T instance)
            where T: class
        {
            return Remove(connection, instance, DefaultTableNameMapper);
        }

        /// <summary>
        /// Deletes an object form the database by it's primary key.
        /// </summary>
        /// <typeparam name="T">The type of the objects.</typeparam>
        /// <param name="connection">The database connection.</param>
        /// <param name="instance">The object to delete</param>
        /// <param name="tableNameMapper">
        /// The function that maps a CLR class name to a database table name.
        /// </param>
        /// <returns><c>true</c> if the object has been successfuly deleted; <c>false</c> otherwise.</returns>
        public static bool Remove<T>(this IDbConnection connection, T instance, Func<string, string> tableNameMapper)
            where T: class
        {
            if(null == connection)
                throw new ArgumentNullException("connection", Properties.Resources.ErrNullParam);
            if(null == instance)
                throw new ArgumentNullException("instance", Properties.Resources.ErrNullParam);
            if(null == tableNameMapper)
                throw new ArgumentNullException("tableNameMapper", Properties.Resources.ErrNullParam);

            TypeMapping tm = GetMappingForType(typeof(T));
            PropertyMapping primaryKeyMapping = tm.PrimaryKey;
            using(IDbCommand command = tm.CreateDeleteCommand(connection, tableNameMapper))
            {
                PushToDb(instance, primaryKeyMapping, command, 0);
                return 0 != command.ExecuteNonQuery();
            }
        }

        /// <summary>
        /// Removes 0 or more objects form the database storage,
        /// </summary>
        /// <typeparam name="T">The type of the objects to delete.</typeparam>
        /// <param name="connection">The database connection.</param>
        /// <param name="columnName">The name of the property/column.</param>
        /// <param name="value">The value of the property that states the object/row has to be deleted.</param>
        /// <returns>
        /// <c>true</c> if the records ahve been successfully deleted; <c>false</c> otherwise.
        /// </returns>
        /// <remarks>
        /// This method is dangerous becuase it may delete more than one record. Use it only for deleting
        /// foreign keys.
        /// </remarks>
        public static bool RemoveBy<T>(this IDbConnection connection, string columnName, object value)
            where T: class
        {
            return RemoveBy<T>(connection, columnName, value, DefaultTableNameMapper);
        }

        /// <summary>
        /// Removes 0 or more objects form the database storage,
        /// </summary>
        /// <typeparam name="T">The type of the objects to delete.</typeparam>
        /// <param name="connection">The database connection.</param>
        /// <param name="columnName">The name of the property/column.</param>
        /// <param name="value">The value of the property that states the object/row has to be deleted.</param>
        /// <param name="tableNameMapper">
        /// The function that maps a CLR class name to a database table name.
        /// </param>
        /// <returns>
        /// <c>true</c> if the records ahve been successfully deleted; <c>false</c> otherwise.
        /// </returns>
        /// <remarks>
        /// This method is dangerous becuase it may delete more than one record. Use it only for deleting
        /// foreign keys.
        /// </remarks>
        public static bool RemoveBy<T>(this IDbConnection connection, string columnName, object value, Func<string, string> tableNameMapper)
            where T: class
        {
            if(null == connection)
                throw new ArgumentNullException("connection", Properties.Resources.ErrNullParam);
            if(String.IsNullOrEmpty(columnName))
                throw new ArgumentNullException("columnName", Properties.Resources.ErrNullParam);
            if(null == tableNameMapper)
                throw new ArgumentNullException("tableNameMapper", Properties.Resources.ErrNullParam);

            TypeMapping tm = GetMappingForType(typeof(T));
            PropertyMapping field = tm.Properties[columnName];
            if(null == field)
                throw new ObjectMappingException(String.Format(Properties.Resources.ErrNoMapping, columnName, tm.Type.FullName));
            using(IDbCommand command = tm.CreateDeleteByCommand(connection, field, tableNameMapper))
            {
                ((IDataParameter)command.Parameters[0]).Value = value;
                return 0 != command.ExecuteNonQuery();
            }
        }

        //// Implementation, doesn't check parameters
        static bool PutImpl<T>(IDbConnection connection, T instance, Func<string, string> tableNameMapper) where T: class
        {
            //// If the record is clean, do nothing
            if(!IsDirty(instance))
                return false;
            TypeMapping tm = GetMappingForType(typeof(T));
            PropertyMapping primaryKeyMapping = tm.PrimaryKey;
            using(IDbCommand command = tm.CreateUpdateCommand(connection, tableNameMapper))
            {
                int index = 0;
                //// Skip primary key
                foreach(var kv in tm.Properties)
                {
                    if(primaryKeyMapping == kv.Value)
                        continue;
                    PushToDb(instance, kv.Value, command, index++);
                }
                //// Only primary key
                foreach(var kv in tm.Properties)
                {
                    if(primaryKeyMapping == kv.Value)
                    {
                        PushToDb(instance, kv.Value, command, index);
                        break;
                    }
                }
                int result = command.ExecuteNonQuery();
                if(0 != result)
                {
                    Clean(instance);
                    Touch(instance);
                    return true;
                }
                else
                    return false;
            }
        }

        /// <summary>
        /// Updates an object in the database.
        /// </summary>
        /// <typeparam name="T">The type of the objects.</typeparam>
        /// <param name="connection">The database connection.</param>
        /// <param name="instance">The object to insert into the database.</param>
        /// <returns><c>true</c> if the object has been successfully updated; <c>false</c> otherwise.</returns>
        public static bool Put<T>(this IDbConnection connection, T instance)
            where T: class
        {
            return Put<T>(connection, instance, DefaultTableNameMapper);
        }

        /// <summary>
        /// Updates an object in the database.
        /// </summary>
        /// <typeparam name="T">The type of the objects.</typeparam>
        /// <param name="connection">The database connection.</param>
        /// <param name="instance">The object to insert into the database.</param>
        /// <param name="tableNameMapper">
        /// The function that maps a CLR class name to a database table name.
        /// </param>
        /// <returns><c>true</c> if the object has been successfully updated; <c>false</c> otherwise.</returns>
        public static bool Put<T>(this IDbConnection connection, T instance, Func<string, string> tableNameMapper)
            where T: class
        {
            if(null == connection)
                throw new ArgumentNullException("connection", Properties.Resources.ErrNullParam);
            if(null == instance)
                throw new ArgumentNullException("instance", Properties.Resources.ErrNullParam);
            if(null == tableNameMapper)
                throw new ArgumentNullException("tableNameMapper", Properties.Resources.ErrNullParam);

            return PutImpl<T>(connection, instance, tableNameMapper);
        }

        /// <summary>
        /// Updates a collection of data objects at once.
        /// </summary>
        /// <typeparam name="T">The type of the objects.</typeparam>
        /// <param name="connection">The database connection.</param>
        /// <param name="collection">A collection of the <typeparamref name="T"/> data objects.</param>
        public static void PutRange<T>(this IDbConnection connection, IEnumerable<T> collection)
            where T: class
        {
            PutRange<T>(connection, collection, DefaultTableNameMapper);
        }

        /// <summary>
        /// Updates a collection of data objects at once.
        /// </summary>
        /// <typeparam name="T">The type of the objects.</typeparam>
        /// <param name="connection">The database connection.</param>
        /// <param name="collection">A collection of the <typeparamref name="T"/> data objects.</param>
        /// <param name="tableNameMapper">
        /// The function that maps a CLR class name to a database table name.
        /// </param>
        public static void PutRange<T>(this IDbConnection connection, IEnumerable<T> collection, Func<string, string> tableNameMapper)
            where T: class
        {
            if(null == connection)
                throw new ArgumentNullException("connection", Properties.Resources.ErrNullParam);
            if(null == collection)
                throw new ArgumentNullException("collection", Properties.Resources.ErrNullParam);
            if(null == tableNameMapper)
                throw new ArgumentNullException("tableNameMapper", Properties.Resources.ErrNullParam);

            using(IDbTransaction transaction = connection.BeginTransaction())
            {
                try
                {
                    foreach(var instance in collection)
                        PutImpl<T>(connection, instance, tableNameMapper);
                }
                catch
                {
                    transaction.Rollback();
                    throw;
                }
                transaction.Commit();
            }
        }
    }
}
